package com.lancoo.ccas52.arrangecourse.coursescheduling.assignlist;

import com.lancoo.ccas52.arrangecourse.common.ClassHourUtil;
import com.lancoo.ccas52.arrangecourse.coursescheduling.allocatetimeslotroom.AllocateTimeslotRoom;
import com.lancoo.ccas52.arrangecourse.dataprocess.GenerateSchedulingData;
import com.lancoo.ccas52.arrangecourse.entities.ClassHour;
import com.lancoo.ccas52.arrangecourse.entities.TeachingClassUnit;
import com.lancoo.ccas52.arrangecourse.entities.TimeslotRoom;
import com.lancoo.ccas52.arrangecourse.rulecheck.RuleService;
import com.lancoo.ccas52.pojo.dto.ArrangeTeachingClassDto;
import lombok.extern.slf4j.Slf4j;

import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;

/**
 * @Description 常规排课类
 * @Author Fortysun
 * @Date 2023/9/4 16:10
 **/
@Slf4j
public abstract class NormalAssign extends GenerateSchedulingData {
    //定义分配教室课时分组的对象
    private SpecifiedTimelotRoom specifiedTimelotRoom;

    /**
     * 接收外面传入的排课规则，以及教室课时分组的分配规则；
     *
     * @param ruleService          排课规则检测类
     * @param allocateTimeslotRoom 教室课时分组分配处理；
     */
    public NormalAssign(RuleService ruleService,
                        AllocateTimeslotRoom allocateTimeslotRoom) {
        //初始化，按教室类型，处理教室课时分组信息列表的对象
        if (specifiedTimelotRoom == null) {
            specifiedTimelotRoom = new SpecifiedTimelotRoom(ruleService, allocateTimeslotRoom);
        }
    }

    /**
     * 将教学班排课单元，转换为ClassHour课表课时对象，并添加到map，方便用来判断排课冲突以及后面的冲突计算
     *
     * @param teachingClassUnit 教学班排课单元
     * @param classHourMap      课表课时map
     */
    protected abstract ConcurrentHashMap<String, CopyOnWriteArrayList<ClassHour>> GenerateClassHour(TeachingClassUnit teachingClassUnit,
                                                                                                    ConcurrentHashMap<String, CopyOnWriteArrayList<ClassHour>> classHourMap);

    /**
     * 支持设置规则的检测
     *
     * @param ruleService 规则的检测服务，检测规则是否符合要求
     */
    public void setRuleService(RuleService ruleService) {
        specifiedTimelotRoom.setRuleService(ruleService);
    }

    /**
     * 开始指定排课
     *
     * @param taskId                排课计划id
     * @param teachingClassList     教学班列表
     * @param teachingClassUnitList 尽量排课剩下的，未分配的教学班排课单元
     * @param timeslotRoomList      排课使用的教室课时分组信息
     * @param classHourMap          锁定的正常课程，转换过来的排课单元map对象，key为timeslotCode
     * @return
     */
    public LinkedList<TeachingClassUnit> start(Integer taskId,
                                               List<ArrangeTeachingClassDto> teachingClassList,
                                               LinkedList<TeachingClassUnit> teachingClassUnitList,
                                               LinkedList<TimeslotRoom> timeslotRoomList,
                                               ConcurrentHashMap<String, CopyOnWriteArrayList<ClassHour>> classHourMap) {
        //获取已经生成的课表对象数量，后面的编号从后面开始
        Integer startPos = ClassHourUtil.getMapElementSize(classHourMap);
        //将教学班列表，以及前面排课，剩余的排课教学班单元信息，一并合在一起，
        // 生成排课教学班排课单元Map，key为教学班所需的教室类型
        Map<Integer, LinkedList<TeachingClassUnit>> teachingClassUnitMap = getTeachingClassUnitMap(taskId,
                teachingClassList, startPos, teachingClassUnitList);
        //按教室类型，将教室课时分组列表数据，进行分组
        Map<Integer, LinkedList<TimeslotRoom>> timeslotRoomMap = getTimeslotRoomMap(timeslotRoomList);
        //为了解决lamda表达式，提示需要用final变量的问题
        Map<Integer, LinkedList<TimeslotRoom>> roomMap = timeslotRoomMap;
        //定义返回值列表
        LinkedList<TeachingClassUnit> retList = new LinkedList<>();
        if (teachingClassUnitMap != null && !teachingClassUnitMap.isEmpty()
                && timeslotRoomMap != null && !timeslotRoomMap.isEmpty()) {
            //线程池大小
            int size = teachingClassUnitMap.size();
            log.info("#NormalAssign.start#--线程池大小为：{}", size);
            // 创建一个固定大小的线程池,size大小
            ExecutorService executor = Executors.newFixedThreadPool(size);
            // 创建一个CyclicBarrier，设置屏障点为size
            CyclicBarrier barrier = new CyclicBarrier(size);
            if (teachingClassUnitMap != null
                    && !teachingClassUnitMap.isEmpty()) {
                //开始为排课单元分配教室课时分组信息
                for (Map.Entry<Integer, LinkedList<TeachingClassUnit>> entry : teachingClassUnitMap.entrySet()) {
                    executor.submit(() -> {
                        try {
                            //获取当前处理的教室类型，为当前教室类型需求的教学班，分配制定教室类型的教室课时分组信息
                            Integer roomType = entry.getKey();
                            log.info("#NormalAssign.start#--开始为教室类型为：{}的教学班分配教室课时分组信息", roomType);
                            //遍历map中指定教室类型，对应的教学班列表
                            LinkedList<TeachingClassUnit> roomTypeUnitList = entry.getValue();
                            //获取map中，指定教室类型的教室列表信息；
                            LinkedList<TimeslotRoom> roomTypeRoomList = roomMap.get(roomType);
                            log.info("#NormalAssign.start#--教室类型为：{}的排课单元数量：{}，待分配的教室课时分组数量：{}",
                                    roomType, (roomTypeUnitList != null ? roomTypeUnitList.size() : 0), (roomTypeRoomList != null ? roomTypeRoomList.size() : 0));
                            //按照指定的教室类型，处理指定教室类型需求的教学班的教室课时分组信息
                            roomTypeUnitList = specifiedTimeslotRoomByRoomType(
                                    roomTypeUnitList,
                                    roomTypeRoomList,
                                    classHourMap,
                                    timeslotRoomList);
                            //如果还剩余未排课的教学班排课单元
                            if (roomTypeUnitList != null && !roomTypeUnitList.isEmpty()) {
                                //添加到返回值列表
                                retList.addAll(roomTypeUnitList);
                            }

                            log.info("#NormalAssign.start#--教室类型为：{}的教学班，分配教室课时分组信息结束,排课单元剩余：{}，教室课时分组剩余：{}",
                                    roomType, (roomTypeUnitList != null ? roomTypeUnitList.size() : 0), (roomTypeRoomList != null ? roomTypeRoomList.size() : 0));
                        } catch (Exception e) {
                            log.error("#NormalAssign.start#--executor.submit出现异常：" + e);
                            e.printStackTrace();
                        } finally {
                            try {
                                // 等待所有任务完成
                                barrier.await();
                            } catch (InterruptedException | BrokenBarrierException e) {
                                log.error("#NormalAssign.start#--barrier.await出现异常：" + e);
                                e.printStackTrace();
                            }
                        }
                    });
                }

                // 关闭线程池，此时线程池不再接受新的任务，但会继续处理已提交的任务
                executor.shutdown();
                try {
                    // 阻塞主线程，等待所有任务完成或超时
                    boolean tasksCompleted = executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
                    if (tasksCompleted) {
                        log.error("#NormalScheduling.startScheduling#--All tasks completed");
                    } else {
                        log.error("#NormalScheduling.startScheduling#--Timeout occurred while waiting for tasks to complete");
                    }
                } catch (InterruptedException e) {
                    log.error("#NormalScheduling.startScheduling#--executor.awaitTermination出现异常：" + e);
                    // 处理中断异常
                    e.printStackTrace();
                }
            }
        }


        if (teachingClassUnitMap != null
                && !teachingClassUnitMap.isEmpty()) {
            teachingClassUnitMap.clear();
            teachingClassUnitMap = null;
        }

        if (timeslotRoomMap != null
                && !timeslotRoomMap.isEmpty()) {
            timeslotRoomMap.clear();
            timeslotRoomMap = null;
        }

        return retList;
    }

    /**
     * 处理指定教室类型的
     *
     * @param teachingClassUnitList 排课教学班单元列表
     * @param timeslotRoomList      所属教室类型的，教室课时分组M列表
     * @param classHourMap          已经排好课的课表信息
     * @param orgTimeslotRoomList   原始的教室课时分组信息列表，应该删除掉已经分配的，所以传入进行处理
     * @return
     */
    private LinkedList<TeachingClassUnit> specifiedTimeslotRoomByRoomType(LinkedList<TeachingClassUnit> teachingClassUnitList,
                                                                          LinkedList<TimeslotRoom> timeslotRoomList,
                                                                          ConcurrentHashMap<String, CopyOnWriteArrayList<ClassHour>> classHourMap,
                                                                          LinkedList<TimeslotRoom> orgTimeslotRoomList) {
        //如果当前分类的教学班列表为空，或者没有数据，那么进行返回
        if (teachingClassUnitList == null
                || teachingClassUnitList.isEmpty()) {
            return null;
        }
        if (timeslotRoomList == null
                || timeslotRoomList.isEmpty()) {
            return teachingClassUnitList;
        }
        Collections.shuffle(teachingClassUnitList);
        Collections.sort(teachingClassUnitList);

        //先对排课单元进行乱序，这样在下面的排序的结果
        //可以实现相对无序，因为相同容量大小的排课单元比较多
        //所以顺序可以变化
        Collections.shuffle(timeslotRoomList);
        //以下将两个列表都按照从小到大排序，可以实现将排课单元安排在合理的位置
        //比如学生人数为60，如果教室课时分组中，教室的容量是60、61、70，可以确保，精确匹配60
        //同时因为每匹配到一个元素，教室课时分组列表，会删除一个元素，所以整个列表会慢慢变短，能够提升查找的效率，
        //如果是从大到小排序，可以一开始学生人数为120，没有教室有这么大容量，那么需要遍历整个列表
        //然后按照教室容量，从低到高排序
        Collections.sort(timeslotRoomList);


        //先根据教室容量进行分组 再根据教室id进行分组
        TreeMap<Integer, TreeMap<Long, LinkedList<TimeslotRoom>>> timeslotRoomMapByCapacityRoomId = timeslotRoomList.stream().collect(Collectors.groupingBy(TimeslotRoom::getCapacity, TreeMap::new,
                Collectors.groupingBy(TimeslotRoom::getRoomId, TreeMap::new, Collectors.toCollection(LinkedList::new))));
        //定义迭代轮次，总迭代次数
        int totalRound = timeslotRoomMapByCapacityRoomId.size();
        //当前迭代次数
        int currentRound = 1;
        while (totalRound >= currentRound) {
            //定义列表遍历的迭代器
            Iterator iterator = teachingClassUnitList.iterator();
            while (iterator.hasNext()) {
                TeachingClassUnit teachingClassUnit = (TeachingClassUnit) iterator.next();

                TreeMap<Long, LinkedList<TimeslotRoom>> timeslotRoomMap = getSpecifiedTimeslotRoomByTeachingClassSize(timeslotRoomMapByCapacityRoomId, currentRound, teachingClassUnit);
                //按教室，对教学班排课单元，进行教室课时分组信息的分配
                teachingClassUnit = specifiedTimeslotRoomByRoomId(teachingClassUnit, timeslotRoomMap, classHourMap);

                //获取分配的map对象；
                LinkedList<TimeslotRoom> specifiedTimeslotRoomList = teachingClassUnit.getTimeslotRoomList();

                //如果已经分配了教室课时分组信息列表
                if (specifiedTimeslotRoomList != null
                        && !specifiedTimeslotRoomList.isEmpty()) {

                    //将教学班排课单元，拆分到ClassHour课表课时中，进行排课冲突的判断，以及后面的冲突检测
                    classHourMap = GenerateClassHour(teachingClassUnit, classHourMap);
                    //已经分配的教学班，直接将其从列表删除
                    //剩下未分配的，在第二轮中，进行匹配(冲突匹配，只要连上节次相符，就分配)
                    iterator.remove();
                    //如果是可删除的教室课时分组，需要从当前列表中删除。 该列表后续没有用到，应该不需要处理？
                    timeslotRoomList = removeTimeslotRoomList(timeslotRoomList, specifiedTimeslotRoomList);
                    //如果是可删除的教室课时分组，需要从原始的列表中删除
                    orgTimeslotRoomList = removeTimeslotRoomList(orgTimeslotRoomList, specifiedTimeslotRoomList);
                    //多班级上课新增 原始列表课时
                    addSortTimeslotRoomList(orgTimeslotRoomList, teachingClassUnit.getAddTimeslotRoomList());
                    teachingClassUnit.setAddTimeslotRoomList(null);
                }
            }
            currentRound++;
        }

        return teachingClassUnitList;
    }

    /**
     * 获取可选教室课时分组列表
     *
     * @param timeslotRoomMapByCapacityRoomId 教室课时分组列表
     * @param currentRound                    迭代轮次
     * @param teachingClassUnit               教学班排课单元
     */
    public TreeMap<Long, LinkedList<TimeslotRoom>> getSpecifiedTimeslotRoomByTeachingClassSize(TreeMap<Integer, TreeMap<Long, LinkedList<TimeslotRoom>>> timeslotRoomMapByCapacityRoomId, int currentRound, TeachingClassUnit teachingClassUnit) {
        //默认档次为1
        int level = 1;
        //第一次迭代需要判断当前教学班所属档次
        if (currentRound == 1) {
            for (Integer capacity : timeslotRoomMapByCapacityRoomId.keySet()) {
                if (teachingClassUnit.getTeachingClassSize() <= capacity) {
                    break;
                }
                level++;
            }
            teachingClassUnit.setLevel(level);
        } else {
            level = teachingClassUnit.getLevel();
        }
        //根据档次获取对应教室课时分组信息
        int index = 1;
        for (Integer key : timeslotRoomMapByCapacityRoomId.keySet()) {
            if (index == level + currentRound - 1) {
                return timeslotRoomMapByCapacityRoomId.get(key);
            }
            index++;
        }
        return new TreeMap<>();
    }

    /**
     * 将教室课时信息，按照roomid的形式，存入map
     * 这里使用TreeMap，是为了保证map的有序性
     * 因为之前按照教室容量从低到高，做好了排序了
     * 需要保证这种顺序
     *
     * @param timeslotRoomList 教室课时分组列表
     * @return
     */
    public TreeMap<Long, LinkedList<TimeslotRoom>> timeslotRoomList2MapByRoomId(LinkedList<TimeslotRoom> timeslotRoomList) {

        //定义返回的map类
        TreeMap<Long, LinkedList<TimeslotRoom>> timeslotRoomMap = new TreeMap<>();
        if (timeslotRoomList != null && !timeslotRoomList.isEmpty()) {
            //遍历教室课时分组信息列表
            for (TimeslotRoom timeslotRoom : timeslotRoomList) {
                Long key = timeslotRoom.getRoomId();
                if (timeslotRoomMap.get(key) == null) {
                    LinkedList<TimeslotRoom> mapList = new LinkedList<>();
                    mapList.add(timeslotRoom);
                    timeslotRoomMap.put(key, mapList);
                } else {
                    timeslotRoomMap.get(key).add(timeslotRoom);
                }
            }
        }

        return timeslotRoomMap;
    }

    /**
     * 为教学班分配教室课时，按照单个教室，分配的课时，去进行分配
     *
     * @param teachingClassUnit 教学班排课单元
     * @param timeslotRoomMap   教室课时分组信息map
     * @param classHourMap      课表课时map
     * @return
     */
    public TeachingClassUnit specifiedTimeslotRoomByRoomId(TeachingClassUnit teachingClassUnit,
                                                           TreeMap<Long, LinkedList<TimeslotRoom>> timeslotRoomMap,
                                                           ConcurrentHashMap<String, CopyOnWriteArrayList<ClassHour>> classHourMap) {

        //遍历教室课时分组的map，map的key是教室id，即一间教室、一间教室的进行处理和分配
        for (Map.Entry<Long, LinkedList<TimeslotRoom>> entry : timeslotRoomMap.entrySet()) {
            //获取教学班指定需要分配的教室
            Long specifiedRoomId = teachingClassUnit.getRoomId();
            //如果教学班没有指定排课教室，或者指定了排课的教室
            if (specifiedRoomId == null ||
                    (specifiedRoomId != null && specifiedRoomId.equals(entry.getKey()))) {
                //获取指定教室的，教室课时分组信息列表
                LinkedList<TimeslotRoom> roomList = entry.getValue();

                //查找符合条件的教室课时分组信息列表，分配给教学班排课单元
                teachingClassUnit = specifiedTimelotRoom.startAllocation(teachingClassUnit, roomList, classHourMap);

                //获取分配的map信息
                LinkedList<TimeslotRoom> timeslotList = teachingClassUnit.getTimeslotRoomList();
                //如果教室课时分组信息，分配成功
                if (timeslotList != null
                        && !timeslotList.isEmpty()) {
                    //如果是可删除的教室课时分组，需要从当前列表中删除
                    roomList = removeTimeslotRoomList(roomList, timeslotList);
                    //先删除 再添加新增的课时。
                    addSortTimeslotRoomList(roomList, teachingClassUnit.getAddTimeslotRoomList());

                    break;
                }
            }
        }

        return teachingClassUnit;
    }

    /**
     * 回溯算法 根据课时信息，找到最接近目标值的教学班组合
     *
     * @param teachingClassUnitList
     * @param timeslotRoom
     * @param index                 当前递归层次处理的索引
     * @param currentList           当前已经选择的值的列表
     * @param closestList           存储最接近目标值的和的组合
     * @param closestSum            存储最接近目标值的和
     */
    public static void findCombinedTeachingClassUnitHelp(LinkedList<TeachingClassUnit> teachingClassUnitList,
                                                         TimeslotRoom timeslotRoom,
                                                         int index,
                                                         List<TeachingClassUnit> currentList,
                                                         List<TeachingClassUnit> closestList,
                                                         int[] closestSum) {
        //当前已经选择的值的和，教学班人数和
        int sum = currentList.stream().mapToInt(TeachingClassUnit::getTeachingClassSize).sum();
        //当前已经选择的值的个数，保证所选组合中个数和教室可容纳班级数相等
        int count = currentList.size();

        //递归结束条件
        if (index == teachingClassUnitList.size() || count == timeslotRoom.getAccommodateClassNum()) {
            //如果当前已经选择的值的个数和教室可容纳班级数相等
            if (count == timeslotRoom.getAccommodateClassNum() && sum <= timeslotRoom.getCapacity() && sum > closestSum[0]
                    && isAllocationComplete(currentList)) {
                closestSum[0] = sum;
                closestList.clear();
                closestList.addAll(currentList);
            }
            return;
        }

        // 选择包含当前值
        TeachingClassUnit teachingClassUnit = teachingClassUnitList.get(index);
        if (sum + teachingClassUnit.getTeachingClassSize() <= timeslotRoom.getCapacity()) {
            currentList.add(teachingClassUnit);
            findCombinedTeachingClassUnitHelp(teachingClassUnitList, timeslotRoom, index + 1, currentList, closestList, closestSum);
            currentList.remove(currentList.size() - 1);
        }

        // 选择不包含当前值
        findCombinedTeachingClassUnitHelp(teachingClassUnitList, timeslotRoom, index + 1, currentList, closestList, closestSum);
    }

    public static boolean isAllocationComplete(List<TeachingClassUnit> currentList) {
        //保证组合教学班单元周次 连上次数一致
        if (currentList.isEmpty()) {
            return true;
        }
        TeachingClassUnit firstTeachingClassUnit = currentList.get(0);
        for (TeachingClassUnit teachingClassUnit : currentList) {
            if (!firstTeachingClassUnit.getClassWeeks().equals(teachingClassUnit.getClassWeeks())
                    || !firstTeachingClassUnit.getConnectSection().equals(teachingClassUnit.getConnectSection())) {
                return false;
            }
        }
        return true;
    }
}
